/*
 * Copyright (c) 2009 Jostein Austvik Jacobsen
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package no.yr;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;

import javax.xml.parsers.*;
import java.io.InputStream;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.bind.DatatypeConverter;
import org.w3c.dom.*;
public class Forecast
{
	public String name, type, country, timezone;
	public int utc_offsetMinutes;
	public double altitude, latitude, longitude;
	public String geobase, geobase_id;
	public Date lastUpdate, nextUpdate, sunrise, sunset;
	public ArrayList<WeatherTabular> weatherTabular;
	public ArrayList<WeatherText> weatherText;
	public String weatherDataInfoText, weatherDataInfoUrl;
	
	public void getForecastFromInputStream(InputStream s) throws Exception
	{
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();
		
		Document document = builder.parse(s);
		NodeList root = document.getChildNodes();
		if (root.getLength() <= 0) {
			throw new Exception("No XML-nodes found");
		}
		NodeList weatherdata = root.item(0).getChildNodes(); // Assume there's only one
		
		for (int docpart = 0; docpart < weatherdata.getLength(); docpart++) {
			Node docNode = weatherdata.item(docpart);
			
			// Location
			if (docNode.getNodeName().equalsIgnoreCase("location")) {
				NodeList location = docNode.getChildNodes();
				
				for (int n = 0; n < location.getLength(); n++) {
					Node currentChild = location.item(n);
					
					if (currentChild.getNodeName().equalsIgnoreCase("name")) {
						this.name = currentChild.getTextContent();
					}
					
					else if (currentChild.getNodeName().equalsIgnoreCase("type")) {
						this.type = currentChild.getTextContent();
					}
					
					else if (currentChild.getNodeName().equalsIgnoreCase("country")) {
						this.country = currentChild.getTextContent();
					}
					
					else if (currentChild.getNodeName().equalsIgnoreCase("timezone")) {
						NamedNodeMap attr = currentChild.getAttributes();
						try {
							this.timezone = attr.getNamedItem("id").getTextContent();
							this.utc_offsetMinutes = Integer.parseInt(attr.getNamedItem("utcoffsetMinutes").getTextContent());
						} catch (NullPointerException e) {
							throw new Exception("timezone id and/or utcoffsetMinutes not defined");
						}
					}
					
					else if (currentChild.getNodeName().equalsIgnoreCase("location")) {
						NamedNodeMap attr = currentChild.getAttributes();
						try {
							this.altitude = Double.parseDouble(attr.getNamedItem("altitude").getTextContent());
							this.latitude = Double.parseDouble(attr.getNamedItem("latitude").getTextContent());
							this.longitude = Double.parseDouble(attr.getNamedItem("longitude").getTextContent());
							this.geobase = attr.getNamedItem("geobase").getTextContent();
							this.geobase_id = attr.getNamedItem("geobaseid").getTextContent();
						} catch (NullPointerException e) {
							throw new Exception("altitude, latitude, longitude, geobase and/or geobaseid not defined");
						}
					}
				}
			}
			
			// Weather data source reference
			// We must include the text from credit->link->text and
			// the url from links->id=overview->url where the forecast
			// is displayed. However, links->id=overview->url seems to
			// always be the same as credit->link->url so we just use
			// that last one and skip the whole parsing of the links-node
			if (docNode.getNodeName().equalsIgnoreCase("credit")) {
				NodeList links = docNode.getChildNodes();
				
				for (int n = 0; n < links.getLength(); n++) {
					Node link = links.item(n);
					
					if (link.getNodeName().equalsIgnoreCase("link")) {
						NamedNodeMap attr = link.getAttributes();
						try {
							this.weatherDataInfoText = attr.getNamedItem("text").getTextContent();
							this.weatherDataInfoUrl = attr.getNamedItem("url").getTextContent();
						} catch (NullPointerException e) {
							throw new Exception("text or url for link not defined");
						}
					}
				}
			}
			
			// last update / next update
			if (docNode.getNodeName().equalsIgnoreCase("meta")) {
				NodeList updates = docNode.getChildNodes();
				
				for (int n = 0; n < updates.getLength(); n++) {
					Node update = updates.item(n);
				
					if (update.getNodeName().equalsIgnoreCase("lastupdate")) {
						try {
							this.lastUpdate = DatatypeConverter.parseDate(update.getTextContent()).getTime();
						} catch (NullPointerException e) {
							throw new Exception("unable to parse lastupdate: "+update.getTextContent());
						}
					}
					
					else if (update.getNodeName().equalsIgnoreCase("nextupdate")) {
						try {
							this.nextUpdate = DatatypeConverter.parseDate(update.getTextContent()).getTime();
						} catch (NullPointerException e) {
							throw new Exception("unable to parse nextupdate"+update.getTextContent());
						}
					}
				}
			}
			
			// sunrise / sunset
			if (docNode.getNodeName().equalsIgnoreCase("sun")) {
				NamedNodeMap attr = docNode.getAttributes();
				try {
					this.sunrise = DatatypeConverter.parseDate(attr.getNamedItem("rise").getTextContent()).getTime();
					this.sunset = DatatypeConverter.parseDate(attr.getNamedItem("set").getTextContent()).getTime();
				} catch (NullPointerException e) {
					throw new Exception("sunrise and/or sunset not defined");
				}
			}
			
			// forecast
			if (docNode.getNodeName().equalsIgnoreCase("forecast")) {
				NodeList type = docNode.getChildNodes();
				
				for (int t = 0; t < type.getLength(); t++) {
					
					// forecast as text
					if (type.item(t).getNodeName().equalsIgnoreCase("text")) {
						NodeList text = type.item(t).getChildNodes();
						
						for (int l = 0; l < text.getLength(); l++) {
							if (text.item(l).getNodeName().equalsIgnoreCase("location")) {
								NamedNodeMap locAttr = text.item(l).getAttributes();
								String locationName;
								try {
									locationName = locAttr.getNamedItem("name").getTextContent();
								} catch (NullPointerException e) {
									throw new Exception("text location name not defined");
								}
								NodeList times = text.item(l).getChildNodes();
								
								for (int i = 0; i < times.getLength(); i++) {
									if (times.item(i).getNodeName().equalsIgnoreCase("time")) {
										WeatherText newText = new WeatherText();
										newText.location = locationName;
										NamedNodeMap attr = times.item(i).getAttributes();
										NodeList time = times.item(i).getChildNodes();
												
										try {
											newText.from = DatatypeConverter.parseDate(attr.getNamedItem("from").getTextContent()).getTime();
											newText.to = DatatypeConverter.parseDate(attr.getNamedItem("to").getTextContent()).getTime();
										} catch (NullPointerException e) {
											throw new Exception("text location from-date and/or to-date not defined");
										}
										
										for (int j = 0; j < time.getLength(); j++) {
											if (time.item(j).getNodeName().equalsIgnoreCase("title")) {
												newText.title = time.item(j).getTextContent();
											}
											else if (time.item(j).getNodeName().equalsIgnoreCase("body")) {
												String body = time.item(j).getTextContent();
												newText.area = body.substring(body.indexOf('>')+1,body.indexOf(':')-1).trim();
												newText.text = body.substring(body.lastIndexOf('>')+1).trim();
											}
										}
										
										weatherText.add(newText);
									}
								}
							}
						}
					}
					
					// tabular forecast (numbers)
					else if (type.item(t).getNodeName().equalsIgnoreCase("tabular")) {
						NodeList tabular = type.item(t).getChildNodes();
						
						for (int i = 0; i < tabular.getLength(); i++) {
							if (tabular.item(i).getNodeName().equalsIgnoreCase("time")) {
								WeatherTabular weatherTabular = new WeatherTabular();
								NamedNodeMap attr = tabular.item(i).getAttributes();
								NodeList time = tabular.item(i).getChildNodes();
								
								int period;
								try {
									weatherTabular.from = DatatypeConverter.parseDate(attr.getNamedItem("from").getTextContent()).getTime();
									weatherTabular.to = DatatypeConverter.parseDate(attr.getNamedItem("to").getTextContent()).getTime();
									period = Integer.parseInt(attr.getNamedItem("period").getTextContent());
								} catch (NullPointerException e) {
									throw new Exception("tabular from-date, to-date and/or period not defined");
								}
								
								switch (period) {
									case 0: weatherTabular.period = WeatherTabular.Period.NIGHT; break;
									case 1: weatherTabular.period = WeatherTabular.Period.MORNING; break;
									case 2: weatherTabular.period = WeatherTabular.Period.DAY; break;
									case 3: weatherTabular.period = WeatherTabular.Period.EVENING; break;
									default: weatherTabular.period = WeatherTabular.Period.UNKNOWN;
								}
								
								for (int j = 0; j < time.getLength(); j++) {
									Node childNode = time.item(j);
									
									if (childNode.getNodeName().equalsIgnoreCase("symbol")) {
										attr = childNode.getAttributes();
										
										int symbol;
										try {
											weatherTabular.symbolName = attr.getNamedItem("name").getTextContent();
											symbol = Integer.parseInt(attr.getNamedItem("number").getTextContent());
										} catch (NullPointerException e) {
											throw new Exception("tabular symbol name and/or number not defined");
										}
										
										switch (symbol) {
											case 1: weatherTabular.symbol = WeatherTabular.Symbol.SUN; break;
											case 2: weatherTabular.symbol = WeatherTabular.Symbol.FAIR; break;
											case 3: weatherTabular.symbol = WeatherTabular.Symbol.PARTLY_CLOUDY; break;
											case 4: weatherTabular.symbol = WeatherTabular.Symbol.CLOUDY; break;
											case 5: weatherTabular.symbol = WeatherTabular.Symbol.RAIN_SHOWERS; break;
											case 6: weatherTabular.symbol = WeatherTabular.Symbol.RAIN_SHOWERS_WITH_THUNDER; break;
											case 7: weatherTabular.symbol = WeatherTabular.Symbol.SLEET_SHOWERS; break;
											case 8: weatherTabular.symbol = WeatherTabular.Symbol.SNOW_SHOWERS; break;
											case 9: weatherTabular.symbol = WeatherTabular.Symbol.RAIN; break;
											case 10: weatherTabular.symbol = WeatherTabular.Symbol.HEAVY_RAIN; break;
											case 11: weatherTabular.symbol = WeatherTabular.Symbol.RAIN_AND_THUNDER; break;
											case 12: weatherTabular.symbol = WeatherTabular.Symbol.SLEET; break;
											case 13: weatherTabular.symbol = WeatherTabular.Symbol.SNOW; break;
											case 14: weatherTabular.symbol = WeatherTabular.Symbol.SNOW_AND_THUNDER; break;
											case 15: weatherTabular.symbol = WeatherTabular.Symbol.FOG; break;
											case 16: weatherTabular.symbol = WeatherTabular.Symbol.SUN_POLAR_NIGHT; break;
											case 17: weatherTabular.symbol = WeatherTabular.Symbol.PARTLY_CLOUDY_POLAR_NIGHT; break;
											case 18: weatherTabular.symbol = WeatherTabular.Symbol.RAIN_SHOWERS_POLAR_NIGHT; break;
											case 19: weatherTabular.symbol = WeatherTabular.Symbol.SNOW_SHOWERS_POLAR_NIGHT; break;
											default: weatherTabular.symbol = WeatherTabular.Symbol.UNKNOWN;
										}
									}
									
									else if (childNode.getNodeName().equalsIgnoreCase("precipitation")) {
										attr = childNode.getAttributes();
										try {
											weatherTabular.precipitation = Double.parseDouble(attr.getNamedItem("value").getTextContent());
										} catch (NullPointerException e) {
											throw new Exception("tabular precipitation value not defined");
										}
									}
									
									else if (childNode.getNodeName().equalsIgnoreCase("windDirection")) {
										attr = childNode.getAttributes();
										String dir;
										try {
											weatherTabular.windDirectionDeg = Double.parseDouble(attr.getNamedItem("deg").getTextContent());
											weatherTabular.windDirectionName = attr.getNamedItem("name").getTextContent();
											dir = attr.getNamedItem("code").getTextContent();
										} catch (NullPointerException e) {
											throw new Exception("tabular wind deg, name and/or code not defined");
										}
										
										if (dir.equals("N")) weatherTabular.windDirection = WeatherTabular.Direction.N;
										else if (dir.equals("NNE")) weatherTabular.windDirection = WeatherTabular.Direction.NNE;
										else if (dir.equals("NE")) weatherTabular.windDirection = WeatherTabular.Direction.NE;
										else if (dir.equals("ENE")) weatherTabular.windDirection = WeatherTabular.Direction.ENE;
										else if (dir.equals("E")) weatherTabular.windDirection = WeatherTabular.Direction.E;
										else if (dir.equals("ESE")) weatherTabular.windDirection = WeatherTabular.Direction.ESE;
										else if (dir.equals("SE")) weatherTabular.windDirection = WeatherTabular.Direction.SE;
										else if (dir.equals("SSE")) weatherTabular.windDirection = WeatherTabular.Direction.SSE;
										else if (dir.equals("S")) weatherTabular.windDirection = WeatherTabular.Direction.S;
										else if (dir.equals("SSW")) weatherTabular.windDirection = WeatherTabular.Direction.SSW;
										else if (dir.equals("SW")) weatherTabular.windDirection = WeatherTabular.Direction.SW;
										else if (dir.equals("WSW")) weatherTabular.windDirection = WeatherTabular.Direction.WSW;
										else if (dir.equals("W")) weatherTabular.windDirection = WeatherTabular.Direction.W;
										else if (dir.equals("WNW")) weatherTabular.windDirection = WeatherTabular.Direction.WNW;
										else if (dir.equals("NW")) weatherTabular.windDirection = WeatherTabular.Direction.NW;
										else if (dir.equals("NNW")) weatherTabular.windDirection = WeatherTabular.Direction.NNW;
										else weatherTabular.windDirection = WeatherTabular.Direction.UNKNOWN;
									}
									
									else if (childNode.getNodeName().equalsIgnoreCase("windSpeed")) {
										attr = childNode.getAttributes();
										try {
											weatherTabular.windSpeedMps = Double.parseDouble(attr.getNamedItem("mps").getTextContent());
											weatherTabular.windSpeedName = attr.getNamedItem("name").getTextContent();
										} catch (NullPointerException e) {
											throw new Exception("tabular wind mps and/or name not defined");
										}
									}
									
									else if (childNode.getNodeName().equalsIgnoreCase("temperature")) {
										attr = childNode.getAttributes();
										try {
											weatherTabular.temperatureUnit = attr.getNamedItem("unit").getTextContent();
											weatherTabular.temperature = Double.parseDouble(attr.getNamedItem("value").getTextContent());
										} catch (NullPointerException e) {
											throw new Exception("tabular temperature unit and/or value not defined");
										}
									}
									
									else if (childNode.getNodeName().equalsIgnoreCase("pressure")) {
										attr = childNode.getAttributes();
										try {
											weatherTabular.pressureUnit = attr.getNamedItem("unit").getTextContent();
											weatherTabular.pressure = Double.parseDouble(attr.getNamedItem("value").getTextContent());
										} catch (NullPointerException e) {
											throw new Exception("tabular pressure unit and/or value not defined");
										}
									}
								}
								
								// associate tabular with corresponding text
								for (int d = 0; d < weatherText.size(); d++) {
									if (weatherTabular.from.compareTo(weatherText.get(d).from) >= 0 &&
										weatherTabular.from.compareTo(new Date(weatherText.get(d).to.getTime()+86400000)) < 0) {
										weatherTabular.text = weatherText.get(d);
										break;
									}
								}
								
								this.weatherTabular.add(weatherTabular);
							}
						}
					}
				}
			}
		}
		Collections.sort(weatherTabular);
		Collections.sort(weatherText);
	}
	
	// Create up-to-date forecast
	// for instance: new Forecast("http://www.yr.no/sted/Norge/Oslo/Oslo/R%C3%B8a~2261695/varsel.xml");
	public Forecast(String url) {
		name = "?";
		type = "?";
		country = "?";
		timezone = "?";
		utc_offsetMinutes = 0;
		altitude = 0.0;
		latitude = 0.0;
		longitude = 0.0;
		geobase = "?";
		geobase_id = "?";
		lastUpdate = new Date();
		nextUpdate = new Date();
		sunrise = new Date();
		sunset = new Date();
		weatherTabular = new ArrayList<WeatherTabular>();
		weatherText = new ArrayList<WeatherText>();
		weatherDataInfoText = "?";
		weatherDataInfoUrl = "?";
		
		
		DeadSimpleHttpClient forecastClient = new DeadSimpleHttpClient(url);
		try {
			InputStream forecastStream = forecastClient.getInputStream();
			getForecastFromInputStream(forecastStream);
		} catch (Exception e) {
			System.err.println(e.toString());
		}
	}
	
	public String toString()
	{
		return this.name;
	}
	
	// Useful for debugging
	public void printDataTree() {
		DateFormat format = DateFormat.getInstance();
		System.out.println(
				"name: "+name+
				"\ntype: "+type+
				"\ncountry: "+country+
				"\ntimezone: "+timezone+
				"\nutc_offsetMinutes: "+utc_offsetMinutes+
				"\naltitude: "+altitude+
				"\nlatitude: "+latitude+
				"\nlongitude: "+longitude+
				"\ngeobase: "+geobase+
				"\ngeobase_id: "+geobase_id+
				"\nlastUpdate: "+format.format(lastUpdate)+
				"\nnextUpdate: "+format.format(nextUpdate)+
				"\nsunrise: "+format.format(sunrise)+
				"\nsunset: "+format.format(sunset)+
				"\nweatherDataInfoText: "+weatherDataInfoText+
				"\nweatherDataInfoUrl: "+weatherDataInfoUrl+
				"\nweatherTabular:"
		);
		
		for (int i = 0; i < weatherTabular.size(); i++) {
			weatherTabular.get(i).printDataTree();
		}
		
	}
}